spb/autoscholar
Public
1import { bibtexFor, getPaperBySlug, plainCitationFor, risFor } from "@autoscholar/core";2import { parseBib } from "@autoscholar/qa";3import { publicArtifactKey } from "@autoscholar/storage";4import type { Metadata } from "next";5import Link from "next/link";6import { notFound } from "next/navigation";7import { CitationPanel } from "@/components/CitationPanel";8import { Disclosure } from "@/components/Disclosure";9import { IconCode, IconData, IconDoc, IconDownload } from "@/components/Icons";10import { MathText } from "@/components/MathText";11import { PaperCard } from "@/components/PaperCard";12import { PaperReader } from "@/components/PaperReader";13import { PublicationMetadata } from "@/components/PublicationMetadata";14import { ResearchProvenance } from "@/components/ResearchProvenance";15import { ScientificFigure } from "@/components/ScientificFigure";16import { SectionHeading } from "@/components/SectionHeading";17import { disciplineLabel, fmtBytes, fmtDate, shortHash } from "@/lib/format";18import { db, storage } from "@/lib/server/db";1920export const dynamic = "force-dynamic";2122async function load(slug: string) {23 try {24 return await getPaperBySlug(db(), slug);25 } catch {26 return null;27 }28}2930export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {31 const { slug } = await params;32 const p = await load(slug);33 if (!p) return { title: "Paper not found" };34 const base = process.env.APP_BASE_URL ?? "http://localhost:8470";35 return {36 title: p.version.title,37 description: p.version.abstract.slice(0, 300),38 alternates: { canonical: `/research/${p.paper.slug}` },39 robots: p.paper.synthetic ? { index: false, follow: false } : undefined,40 openGraph: {41 title: p.version.title,42 description: p.version.abstract.slice(0, 300),43 type: "article",44 url: `${base}/research/${p.paper.slug}`,45 publishedTime: p.version.publishedAt.toISOString(),46 },47 other: {48 citation_title: p.version.title,49 citation_publication_date: p.version.publishedAt.toISOString().slice(0, 10),50 citation_technical_report_number: p.paper.publicId,51 citation_technical_report_institution: "AutoScholar.ai",52 citation_pdf_url: `${base}${p.version.pdfPublicPath}`,53 },54 };55}5657type ArtifactEntry = {58 path: string;59 kind: string;60 sha256: string;61 bytes: number;62 contentType: string;63 publicPath: string;64};65function Dl({ a }: { a: ArtifactEntry }) {66 return (67 <li className="flex items-center justify-between gap-3 border-b py-2 text-sm last:border-0 hairline">68 <a69 href={`${a.publicPath}?download=1`}70 className="min-w-0 truncate font-mono text-xs no-underline hover:underline"71 title={a.path}72 >73 {a.path}74 </a>75 <span className="subtle flex-none text-[11px]">76 {fmtBytes(a.bytes)} · {shortHash(a.sha256, 8)}77 </span>78 </li>79 );80}8182export default async function PaperPage({ params }: { params: Promise<{ slug: string }> }) {83 const { slug } = await params;84 const p = await load(slug);85 if (!p) notFound();86 const base = process.env.APP_BASE_URL ?? "http://localhost:8470";87 const v = p.version;88 const meta = v.metadata as Record<string, unknown>;89 const withdrawn = meta.artifactsWithdrawn === true;90 const set = withdrawn ? [] : v.artifactSet;91 const url = `${base}/research/${p.paper.slug}`;92 const pub = new Date(v.publishedAt);93 const cite = {94 publicId: p.paper.publicId,95 title: v.title,96 year: pub.getUTCFullYear(),97 month: pub.getUTCMonth() + 1,98 versionNo: v.versionNo,99 url,100 keywords: v.keywords,101 abstract: v.abstract,102 };103 const figures = set.filter((a) => a.kind === "figure" && /\.(png|svg)$/i.test(a.path));104 const pdfFigures = set.filter((a) => a.kind === "figure" && /\.pdf$/i.test(a.path));105 const tex = set.filter((a) => a.kind === "manuscript" && /\.(tex|bib|cls|sty)$/i.test(a.path));106 const mainTex = set.find((a) => a.path === "manuscript/main.tex") ?? tex[0];107 const code = set.filter((a) => a.kind === "code");108 const data = set.filter((a) => a.kind === "data");109 const results = set.filter((a) => a.kind === "result" || a.kind === "table");110 const prov = (v.provenance ?? {}) as Record<string, unknown>;111 const qa = (v.qaSummary ?? {}) as {112 gates?: Array<{ gate: string; status: string; severity: string; message: string }>;113 warnings?: string[];114 policyVersion?: string;115 evaluatedAt?: string;116 };117 let bib: ReturnType<typeof parseBib> = [];118 const bibEntry = set.find((a) => a.path === "sources/references.bib");119 if (bibEntry) {120 try {121 bib = parseBib(122 (await storage().get(publicArtifactKey(p.paper.publicId, v.versionNo, bibEntry.path))).toString(123 "utf8",124 ),125 );126 } catch {127 bib = [];128 }129 }130 const jsonLd = {131 "@context": "https://schema.org",132 "@type": "ScholarlyArticle",133 headline: v.title,134 name: v.title,135 abstract: v.abstract,136 keywords: v.keywords.join(", "),137 datePublished: v.publishedAt.toISOString(),138 dateModified: p.paper.updatedAt.toISOString(),139 author: { "@type": "Organization", name: "AutoScholar (AI-generated research)", url: base },140 publisher: { "@type": "Organization", name: "AutoScholar.ai", url: base },141 identifier: p.paper.publicId,142 url,143 isAccessibleForFree: true,144 ...(set.length145 ? {146 encoding: {147 "@type": "MediaObject",148 contentUrl: `${base}${v.pdfPublicPath}`,149 encodingFormat: "application/pdf",150 },151 }152 : {}),153 about: disciplineLabel(p.paper.discipline),154 version: v.versionNo,155 ...(p.retraction ? { creativeWorkStatus: "Retracted" } : {}),156 };157 const verifiedLabel = qa.gates158 ? `${qa.gates.filter((g) => g.status === "pass").length}/${qa.gates.filter((g) => g.status !== "skip").length} gates`159 : undefined;160 const contributions = Array.isArray(meta.contributions) ? (meta.contributions as string[]) : [];161 const sections: Array<[string, string]> = [162 ["Methods", typeof meta.methods === "string" ? meta.methods : ""],163 ["Main results", typeof meta.mainResults === "string" ? meta.mainResults : ""],164 ["Limitations", typeof meta.limitations === "string" ? meta.limitations : ""],165 ].filter(([, v]) => v) as Array<[string, string]>;166 const downloads = withdrawn167 ? []168 : [169 { href: `${v.pdfPublicPath}?download=1`, label: "PDF", icon: <IconDoc size={16} />, primary: true },170 ...(mainTex171 ? [172 {173 href: `${mainTex.publicPath}?download=1`,174 label: "LaTeX source",175 icon: <span className="font-mono text-[11px]">TeX</span>,176 },177 ]178 : []),179 ...(code.length180 ? [{ href: "#artifacts", label: `Code (${code.length})`, icon: <IconCode size={16} /> }]181 : []),182 ...(data.length183 ? [{ href: "#artifacts", label: `Data (${data.length})`, icon: <IconData size={16} /> }]184 : []),185 ];186 return (187 <article className="pb-20 lg:pb-0">188 {/* biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD built from server data */}189 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />190191 {/* Publication header */}192 <div style={{ background: "var(--bg-muted)" }} className="border-b hairline">193 <div className="container-x py-8 sm:py-12">194 {p.retraction && (195 <div196 role="alert"197 className="mb-6 rounded-lg border p-4"198 style={{199 borderColor: "var(--color-danger-500)",200 background: "color-mix(in oklab, var(--color-danger-500) 8%, transparent)",201 }}202 >203 <p className="font-semibold">204 This working paper was retracted on {fmtDate(p.retraction.retractedAt)}.205 </p>206 <p className="mt-1 text-sm">{p.retraction.notice}</p>207 {withdrawn && (208 <p className="muted mt-1 text-sm">209 Its artifacts have been withdrawn from delivery. This page remains as a permanent record.210 </p>211 )}212 </div>213 )}214 <div className="grid gap-8 lg:grid-cols-12">215 <div className="lg:col-span-8">216 <PublicationMetadata217 publicId={p.paper.publicId}218 versionNo={v.versionNo}219 status={p.paper.status}220 synthetic={p.paper.synthetic}221 title={v.title}222 discipline={p.paper.discipline}223 mode={p.paper.researchMode}224 firstPublishedAt={p.paper.firstPublishedAt}225 revisedAt={v.publishedAt}226 keywords={v.keywords}227 verified={verifiedLabel}228 />229 </div>230 <aside className="lg:col-span-4" aria-label="Downloads">231 <div className="surface p-4">232 <p className="label mb-3">Artifacts</p>233 {downloads.length === 0 ? (234 <p className="muted text-sm">Artifacts withdrawn.</p>235 ) : (236 <ul className="grid grid-cols-2 gap-2 lg:grid-cols-1 [&>li:only-child]:col-span-2">237 {downloads.map((dl) => (238 <li key={dl.label}>239 <a240 className={`btn w-full justify-between ${dl.primary ? "btn-primary" : ""}`}241 href={dl.href}242 >243 <span className="inline-flex items-center gap-2">244 {dl.icon}245 {dl.label}246 </span>247 <IconDownload size={14} />248 </a>249 </li>250 ))}251 </ul>252 )}253 <div className="mt-4">254 <Disclosure compact />255 </div>256 </div>257 </aside>258 </div>259 </div>260 </div>261262 <div className="container-x">263 {/* Abstract + findings */}264 <section aria-labelledby="abstract" className="section-tight grid gap-10 lg:grid-cols-12">265 <div className="lg:col-span-8">266 <h2 id="abstract" className="eyebrow">267 Abstract268 </h2>269 <MathText text={v.abstract} className="prose-editorial dropcap mt-3" />270 {contributions.length > 0 && (271 <div className="mt-10">272 <h2 className="eyebrow">Key findings</h2>273 <ol className="mt-3 space-y-3">274 {contributions.map((c, i) => (275 <li key={c} className="grid grid-cols-[2rem_1fr] gap-3">276 <span className="serif mt-0.5 text-xl" style={{ color: "var(--accent)" }}>277 {i + 1}278 </span>279 <MathText text={c} className="prose-paper" />280 </li>281 ))}282 </ol>283 </div>284 )}285 {sections.map(([h, body]) => (286 <div key={h} className="mt-10">287 <h2 className="eyebrow">{h}</h2>288 <MathText text={body} className="prose-paper mt-3" />289 </div>290 ))}291 </div>292 <div className="space-y-4 lg:col-span-4">293 <div className="surface-muted p-4 text-sm">294 <p className="label mb-2">Research status</p>295 <ul className="space-y-1.5">296 <li>AutoScholar working paper (preprint)</li>297 <li>Produced autonomously by an AI research workflow</li>298 <li>299 Automated critical review; <strong>not</strong> human peer review300 </li>301 <li>302 Deterministic quality gates:{" "}303 {qa.gates304 ? `${qa.gates.filter((g) => g.status === "pass").length}/${qa.gates.filter((g) => g.status !== "skip").length} passed`305 : "see provenance"}306 </li>307 <li>DOI: none (not registered)</li>308 </ul>309 </div>310 {p.summary.topics.length > 0 && (311 <div className="surface-muted p-4 text-sm">312 <p className="label mb-2">Topics</p>313 <ul className="flex flex-wrap gap-1.5">314 {p.summary.topics.map((x) => (315 <li key={x.slug}>316 <Link317 href={`/topics/${x.slug}`}318 className="chip no-underline"319 style={{ minHeight: "1.75rem" }}320 >321 {x.name}322 </Link>323 </li>324 ))}325 </ul>326 </div>327 )}328 <nav aria-label="On this page" className="surface-muted hidden p-4 text-sm lg:block">329 <p className="label mb-2">On this page</p>330 <ul className="space-y-1.5">331 {[332 ["#abstract", "Abstract"],333 !withdrawn && ["#pdf", "Read the paper"],334 (figures.length || pdfFigures.length) && ["#figures", "Figures"],335 bib.length && ["#refs", "References"],336 (code.length || data.length || results.length) && ["#artifacts", "Code and data"],337 ["#provenance", "Provenance"],338 ["#cite", "Cite"],339 ["#history", "Versions"],340 ]341 .filter(Boolean)342 .map((x) => {343 const [href, label] = x as [string, string];344 return (345 <li key={href}>346 <a href={href} className="muted no-underline hover:text-[var(--fg)]">347 {label}348 </a>349 </li>350 );351 })}352 </ul>353 </nav>354 </div>355 </section>356357 {/* Reader */}358 {!withdrawn && (359 <section aria-labelledby="pdf" className="section-tight border-t hairline">360 <SectionHeading eyebrow="Manuscript" title="Read the paper" id="pdf">361 Rendered from the released PDF. Use the arrow keys or the controls to turn pages; full-screen is362 available.363 </SectionHeading>364 <div className="mt-6">365 <PaperReader366 src={v.pdfPublicPath}367 title={`${p.paper.publicId} v${v.versionNo} — ${v.title}`}368 downloadHref={`${v.pdfPublicPath}?download=1`}369 versionLabel={`v${v.versionNo}`}370 />371 </div>372 </section>373 )}374375 {/* Figures */}376 {(figures.length > 0 || pdfFigures.length > 0) && (377 <section aria-labelledby="figures" className="section-tight border-t hairline">378 <SectionHeading eyebrow="Evidence" title="Figures" id="figures" tone="sci">379 Figures are the actual files produced by the study, served from the immutable artifact store.380 </SectionHeading>381 <ul className="mt-6 grid gap-5 md:grid-cols-2">382 {figures.map((f, i) => {383 const vec = pdfFigures.find(384 (x) => x.path.replace(/\.pdf$/i, "") === f.path.replace(/\.(png|svg)$/i, ""),385 );386 return (387 <li key={f.path}>388 <ScientificFigure389 src={f.publicPath}390 path={f.path}391 bytes={f.bytes}392 index={i + 1}393 vector={vec ? { href: vec.publicPath, bytes: vec.bytes } : undefined}394 />395 </li>396 );397 })}398 {pdfFigures399 .filter(400 (f) =>401 !figures.some(402 (x) => x.path.replace(/\.(png|svg)$/i, "") === f.path.replace(/\.pdf$/i, ""),403 ),404 )405 .map((f) => (406 <li key={f.path} className="surface p-4 text-sm">407 <a href={`${f.publicPath}?download=1`} className="font-mono text-xs">408 {f.path.replace("results/figures/", "")}409 </a>{" "}410 <span className="subtle">(vector PDF, {fmtBytes(f.bytes)})</span>411 </li>412 ))}413 </ul>414 </section>415 )}416417 {/* References */}418 {bib.length > 0 && (419 <section aria-labelledby="refs" className="section-tight border-t hairline">420 <SectionHeading eyebrow="Bibliography" title={`References (${bib.length})`} id="refs">421 Every reference was verified against a scholarly registry before publication; identifiers link422 to the source of record.423 </SectionHeading>424 <ol className="mt-6 grid gap-x-10 gap-y-3 text-sm md:grid-cols-2">425 {bib.map((b, i) => {426 const f = b.fields;427 const href = f.doi428 ? `https://doi.org/${f.doi.replace(/^https?:\/\/doi\.org\//, "")}`429 : (f.url ?? (f.eprint ? `https://arxiv.org/abs/${f.eprint}` : undefined));430 return (431 <li432 key={b.key}433 id={`ref-${b.key}`}434 className="grid grid-cols-[2rem_1fr] gap-2 border-b pb-3 hairline"435 >436 <span className="subtle font-mono text-xs">[{i + 1}]</span>437 <span className="min-w-0">438 <span>{f.author ? `${f.author.replace(/\s+and\s+/g, ", ")}. ` : ""}</span>439 {f.year && <span>({f.year}). </span>}440 <span className="italic">{f.title?.replace(/[{}]/g, "")}</span>441 {f.journal && <span>. {f.journal}</span>}442 {f.booktitle && <span>. In {f.booktitle}</span>}443 {f.volume && <span>, {f.volume}</span>}444 {f.pages && <span>, {f.pages}</span>}.{" "}445 {href && (446 <a href={href} rel="noopener noreferrer" className="break-all font-mono text-xs">447 {f.doi448 ? `doi:${f.doi.replace(/^https?:\/\/doi\.org\//, "")}`449 : f.eprint450 ? `arXiv:${f.eprint}`451 : "link"}452 </a>453 )}454 </span>455 </li>456 );457 })}458 </ol>459 </section>460 )}461462 {/* Artifacts */}463 {(code.length > 0 || data.length > 0 || results.length > 0) && (464 <section id="artifacts" aria-labelledby="artifacts-h" className="section-tight border-t hairline">465 <SectionHeading466 eyebrow="Reproducibility"467 title="Code, data and results"468 id="artifacts-h"469 tone="sci"470 >471 Only files that were part of the study are listed. Each download carries its SHA-256 digest.472 </SectionHeading>473 <div className="mt-6 grid gap-5 lg:grid-cols-3">474 {[475 ["Code", code, <IconCode key="c" size={16} />],476 ["Data", data, <IconData key="d" size={16} />],477 ["Results and tables", results, <IconDoc key="r" size={16} />],478 ]479 .filter(([, list]) => (list as ArtifactEntry[]).length > 0)480 .map(([label, list, icon]) => (481 <div key={label as string} className="surface p-4">482 <p className="label mb-1 flex items-center gap-2">483 {icon as React.ReactNode} {label as string} ({(list as ArtifactEntry[]).length})484 </p>485 <ul className="max-h-80 overflow-auto">486 {(list as ArtifactEntry[]).map((a) => (487 <Dl key={a.path} a={a} />488 ))}489 </ul>490 </div>491 ))}492 </div>493 </section>494 )}495496 {/* Provenance */}497 <section id="provenance" aria-labelledby="provenance-h" className="section-tight border-t hairline">498 <SectionHeading eyebrow="Transparent record" title="Research provenance" id="provenance-h">499 How this paper was made: models, instruction digest, pipeline stages, verification policy and500 every gate result.501 </SectionHeading>502 <div className="mt-6">503 <ResearchProvenance prov={prov} qa={qa} texReleased={tex.length > 0} meta={meta} />504 </div>505 </section>506507 {/* Cite */}508 <section aria-labelledby="cite" className="section-tight border-t hairline">509 <SectionHeading eyebrow="Citation" title="Cite this working paper" id="cite" />510 <div className="mt-6 max-w-3xl">511 <CitationPanel512 slug={p.paper.slug}513 bibtex={bibtexFor(cite)}514 ris={risFor(cite)}515 plain={plainCitationFor(cite)}516 />517 </div>518 </section>519520 {/* History */}521 <section522 aria-labelledby="history"523 className="section-tight grid gap-8 border-t hairline md:grid-cols-2"524 >525 <div>526 <h2 id="history" className="title-2">527 Version history528 </h2>529 <ol className="mt-4 space-y-2 text-sm">530 {p.versions.map((x) => (531 <li key={x.id} className="surface flex items-start justify-between gap-3 p-3">532 <div>533 <span className="font-medium">Version {x.versionNo}</span>{" "}534 <span className="muted">· {fmtDate(x.publishedAt)}</span>535 <p className="muted mt-1">{x.changelog}</p>536 </div>537 {!withdrawn && (538 <a className="btn btn-sm" href={`${x.pdfPublicPath}?download=1`}>539 PDF v{x.versionNo}540 </a>541 )}542 </li>543 ))}544 </ol>545 </div>546 <div>547 <h2 className="title-2">Corrections and notices</h2>548 {p.corrections.length === 0 && !p.retraction ? (549 <p className="muted mt-4 text-sm">No corrections have been issued.</p>550 ) : (551 <ul className="mt-4 space-y-2 text-sm">552 {p.corrections.map((c) => (553 <li key={c.id} className="surface p-3">554 <span className="font-medium">555 Correction v{c.fromVersionNo} → v{c.toVersionNo}556 </span>{" "}557 <span className="muted">· {fmtDate(c.createdAt)}</span>558 <p className="mt-1">{c.notice}</p>559 </li>560 ))}561 {p.retraction && (562 <li className="surface p-3">563 <span className="font-medium">Retraction</span>{" "}564 <span className="muted">· {fmtDate(p.retraction.retractedAt)}</span>565 <p className="mt-1">{p.retraction.notice}</p>566 </li>567 )}568 </ul>569 )}570 </div>571 </section>572573 {p.related.length > 0 && (574 <section aria-labelledby="related" className="section-tight border-t hairline">575 <SectionHeading eyebrow="Related" title="Related working papers" id="related">576 Selected by shared discipline or keywords recorded in verified metadata.577 </SectionHeading>578 <ul className="mt-6 grid gap-4 md:grid-cols-2">579 {p.related.map((r) => (580 <li key={r.publicId}>581 <PaperCard p={r} />582 </li>583 ))}584 </ul>585 </section>586 )}587 <div className="pb-6">588 <Disclosure />589 </div>590 </div>591592 {/* mobile sticky action bar */}593 {!withdrawn && (594 <div className="mobile-bar flex items-center gap-2 lg:hidden">595 <span className="mono-id min-w-0 flex-1 truncate">596 {p.paper.publicId} · v{v.versionNo}597 </span>598 <a href="#pdf" className="btn btn-sm">599 Read600 </a>601 <a href={`${v.pdfPublicPath}?download=1`} className="btn btn-primary btn-sm">602 <IconDownload size={14} /> PDF603 </a>604 </div>605 )}606 </article>607 );608}609